/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 */

/**
 * This script prepares the code examples and diffs in the documentation.
 *
 * The `raw-loader` will be used by Docusaurus to load file contents.
 * This script generates the diff files. It also trims the file headers.
 */

const child_process = require('child_process');
const fs = require('fs');
const path = require('path');

const EXAMPLE_SRC_ROOT = '../examples';
const EXAMPLE_DEST_ROOT = 'static/examples';
const EXAMPLE_SPECS = [
  {
    name: 'MNISTDemo',
    pathTemplate: idx => `mnist-digit-classification/MNISTDemo.v${idx}.tsx`,
  },
  {
    name: 'ImageClassificationDemo',
    pathTemplate: idx =>
      `image-classification/ImageClassificationDemo.v${idx}.tsx`,
  },
  {
    name: 'QuestionAnsweringDemo',
    pathTemplate: idx => `question-answering/QuestionAnsweringDemo.v${idx}.tsx`,
  },
];

// This class generates a JS component wrapping the diffs and code for a set of examples.
class ExampleModuleBuilder {
  constructor(moduleName) {
    this.moduleName = moduleName;
    this.imports = [];
    this.components = [];
  }

  _preamble() {
    return `/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @format
 */

// This file was generated by \`yarn run codegen-examples\`

import React from 'react';
import CodeBlock from '@theme/CodeBlock';

function ${this.moduleName}() {}
`;
  }

  addCodeBlock(idx, filepath) {
    const importName = `V${idx}Contents`;
    const componentName = `V${idx}CodeBlock`;
    this.imports.push(`import ${importName} from '!!raw-loader!/${filepath}';`);
    this.components.push(
      `${this.moduleName}.${componentName} = function ${componentName}(props) {
  return <CodeBlock {...props} className="language-tsx">{${importName}}</CodeBlock>
};`,
    );
  }

  addDiffBlock(idx, filepath) {
    const importName = `V${idx}DiffContents`;
    const componentName = `V${idx}DiffBlock`;
    this.imports.push(`import ${importName} from '!!raw-loader!/${filepath}';`);
    this.components.push(
      `${this.moduleName}.${componentName} = function ${componentName}(props) {
  return <CodeBlock {...props} className="language-diff">{${importName}}</CodeBlock>
};`,
    );
  }

  _afterword() {
    return `export default ${this.moduleName};\n`;
  }

  build() {
    const parts = [this._preamble()]
      .concat(this.imports)
      .concat(this.components)
      .concat([this._afterword()]);
    return parts.join('\n\n');
  }
}

// Take off the license header. We don't display it in tutorials.
const blankLineRegExp = /\n\s*\n/;
const leadingBlankLineRegExp = /^\s+\n/m;
function trimExampleFileHeader(exampleContents) {
  const firstBlankLineIdx = exampleContents.search(blankLineRegExp);
  if (firstBlankLineIdx !== -1) {
    return exampleContents
      .slice(firstBlankLineIdx)
      .replace(leadingBlankLineRegExp, '');
  }
  return exampleContents;
}

// Take off the diff header comments. We don't display them in tutorials.
function trimDiffHeader(diffContents) {
  return diffContents.split('\n').slice(2).join('\n');
}

function importExamples() {
  for (let spec of EXAMPLE_SPECS) {
    console.log(`\nProcessing ${spec.name}...`);
    const moduleName = `${spec.name}Examples`;
    const moduleBuilder = new ExampleModuleBuilder(moduleName);

    // Delete and remake directory to avoid leaving cruft from prior runs.
    const destDir = path.dirname(
      path.join(EXAMPLE_DEST_ROOT, spec.pathTemplate(0)),
    );
    if (fs.existsSync(destDir)) {
      console.log(`  Removing directory ${destDir}`);
      fs.rmSync(destDir, {recursive: true});
    }
    console.log(`  Making directory ${destDir}`);
    fs.mkdirSync(destDir, {recursive: true});

    // Copy each example from 0..N, trimming the header comment out and
    // generating in-between diffs along the way, from 0->1, 1->2, etc.
    for (let i = 0; ; i++) {
      const sourcePath = path.join(EXAMPLE_SRC_ROOT, spec.pathTemplate(i));
      const destPath = path.join(EXAMPLE_DEST_ROOT, spec.pathTemplate(i));
      const diffPath = path.join(
        EXAMPLE_DEST_ROOT,
        spec.pathTemplate(i) + '.diff',
      );

      if (!fs.existsSync(sourcePath)) {
        break;
      }

      console.log(`  Writing trimmed ${destPath}`);
      const exampleContents = fs.readFileSync(sourcePath, {encoding: 'utf8'});
      const trimmedExampleContents = trimExampleFileHeader(exampleContents);
      fs.writeFileSync(destPath, trimmedExampleContents, {encoding: 'utf8'});
      moduleBuilder.addCodeBlock(i, destPath);

      if (i > 0) {
        console.log(`  Writing trimmed ${diffPath}`);
        const prevPath = path.join(EXAMPLE_DEST_ROOT, spec.pathTemplate(i - 1));
        const diffResult = child_process.spawnSync(
          'diff',
          ['--unified', prevPath, destPath],
          {encoding: 'utf8'},
        );
        const trimmedDiffContents = trimDiffHeader(diffResult.stdout);
        fs.writeFileSync(diffPath, trimmedDiffContents, {encoding: 'utf8'});
        moduleBuilder.addDiffBlock(i, diffPath);
      }
    }

    // Write out React component wrappers for each example contents and diff.
    const modulePath = `src/components/examples/${moduleName}.js`;
    const moduleDir = path.dirname(modulePath);
    const moduleContents = moduleBuilder.build();

    if (!fs.existsSync(moduleDir)) {
      console.log(`  Making directory ${moduleDir}`);
      fs.mkdirSync(moduleDir, {recursive: true});
    }

    console.log(`  Writing React components to ${modulePath}`);
    fs.writeFileSync(modulePath, moduleContents, {encoding: 'utf8'});
  }
}

importExamples();
